import { NextRequest } from "next/server.js";
import { describe, expect, it } from "vitest";

import { ProxyOptions } from "../types/index.js";
import {
  buildForwardedRequestHeaders,
  buildForwardedResponseHeaders,
  transformTargetUrl
} from "./proxy.js";

describe("headers", () => {
  describe("buildForwardedRequestHeaders", () => {
    it("should forward headers from the default allow-list", () => {
      const request = new NextRequest("https://example.com", {
        headers: {
          accept: "application/json",
          "accept-language": "en-US",
          "user-agent": "Mozilla/5.0",
          "x-forwarded-for": "192.168.1.1",
          "x-request-id": "abc123"
        }
      });

      const result = buildForwardedRequestHeaders(request);

      expect(result.get("accept")).toBe("application/json");
      expect(result.get("accept-language")).toBe("en-US");
      expect(result.get("user-agent")).toBe("Mozilla/5.0");
      expect(result.get("x-forwarded-for")).toBe("192.168.1.1");
      expect(result.get("x-request-id")).toBe("abc123");
    });

    it("should not forward headers not in the allow-list", () => {
      const request = new NextRequest("https://example.com", {
        headers: {
          accept: "application/json",
          "some-custom-header": "should-not-be-forwarded",
          authorization: "Bearer token",
          cookie: "session=xyz"
        }
      });

      const result = buildForwardedRequestHeaders(request);

      expect(result.get("accept")).toBe("application/json");
      expect(result.get("some-custom-header")).toBeNull();
      expect(result.get("authorization")).toBeNull();
      expect(result.get("cookie")).toBeNull();
    });

    it("should handle case-insensitive header names", () => {
      const request = new NextRequest("https://example.com", {
        headers: {
          Accept: "application/json",
          "Content-Type": "text/plain",
          "User-Agent": "Mozilla/5.0"
        }
      });

      const result = buildForwardedRequestHeaders(request);

      expect(result.get("accept")).toBe("application/json");
      expect(result.get("content-type")).toBe("text/plain");
      expect(result.get("user-agent")).toBe("Mozilla/5.0");
    });

    it("should handle empty headers", () => {
      const request = new NextRequest("https://example.com");

      const result = buildForwardedRequestHeaders(request);

      expect(Array.from(result.keys()).length).toBe(0);
    });

    it("should forward caching and conditional request headers", () => {
      const request = new NextRequest("https://example.com", {
        headers: {
          "cache-control": "no-cache",
          "if-none-match": '"abc123"',
          "if-modified-since": "Wed, 21 Oct 2015 07:28:00 GMT",
          etag: '"xyz789"'
        }
      });

      const result = buildForwardedRequestHeaders(request);

      expect(result.get("cache-control")).toBe("no-cache");
      expect(result.get("if-none-match")).toBe('"abc123"');
      expect(result.get("if-modified-since")).toBe(
        "Wed, 21 Oct 2015 07:28:00 GMT"
      );
      expect(result.get("etag")).toBe('"xyz789"');
    });

    it("should forward tracing and observability headers", () => {
      const request = new NextRequest("https://example.com", {
        headers: {
          traceparent: "00-abc123-def456-01",
          tracestate: "vendor=value",
          "x-correlation-id": "corr-123"
        }
      });

      const result = buildForwardedRequestHeaders(request);

      expect(result.get("traceparent")).toBe("00-abc123-def456-01");
      expect(result.get("tracestate")).toBe("vendor=value");
      expect(result.get("x-correlation-id")).toBe("corr-123");
    });

    it("should forward proxy headers for IP and rate limiting", () => {
      const request = new NextRequest("https://example.com", {
        headers: {
          "x-forwarded-for": "192.168.1.1, 10.0.0.1",
          "x-forwarded-host": "example.com",
          "x-forwarded-proto": "https",
          "x-real-ip": "192.168.1.1"
        }
      });

      const result = buildForwardedRequestHeaders(request);

      expect(result.get("x-forwarded-for")).toBe("192.168.1.1, 10.0.0.1");
      expect(result.get("x-forwarded-host")).toBe("example.com");
      expect(result.get("x-forwarded-proto")).toBe("https");
      expect(result.get("x-real-ip")).toBe("192.168.1.1");
    });
  });

  describe("buildForwardedResponseHeaders", () => {
    it("should forward all headers except hop-by-hop headers", () => {
      const response = new Response("body", {
        headers: {
          "content-type": "application/json",
          "cache-control": "max-age=3600",
          "x-custom-header": "custom-value",
          etag: '"abc123"'
        }
      });

      const result = buildForwardedResponseHeaders(response);

      expect(result.get("content-type")).toBe("application/json");
      expect(result.get("cache-control")).toBe("max-age=3600");
      // custom headers allowed in response headers
      expect(result.get("x-custom-header")).toBe("custom-value");
      expect(result.get("etag")).toBe('"abc123"');
    });

    it("should strip all hop-by-hop headers", () => {
      const response = new Response("body", {
        headers: {
          "content-type": "application/json",
          connection: "keep-alive",
          "keep-alive": "timeout=5",
          "proxy-authenticate": "Basic",
          "proxy-authorization": "Bearer token",
          te: "trailers",
          trailer: "Expires",
          "transfer-encoding": "chunked",
          upgrade: "h2c"
        }
      });

      const result = buildForwardedResponseHeaders(response);

      expect(result.get("content-type")).toBe("application/json");
      expect(result.get("connection")).toBeNull();
      expect(result.get("keep-alive")).toBeNull();
      expect(result.get("proxy-authenticate")).toBeNull();
      expect(result.get("proxy-authorization")).toBeNull();
      expect(result.get("te")).toBeNull();
      expect(result.get("trailer")).toBeNull();
      expect(result.get("transfer-encoding")).toBeNull();
      expect(result.get("upgrade")).toBeNull();
    });

    it("should handle case-insensitive hop-by-hop headers", () => {
      const response = new Response("body", {
        headers: {
          "content-type": "application/json",
          Connection: "Keep-Alive",
          "Transfer-Encoding": "chunked",
          Upgrade: "WebSocket"
        }
      });

      const result = buildForwardedResponseHeaders(response);

      expect(result.get("content-type")).toBe("application/json");
      expect(result.get("connection")).toBeNull();
      expect(result.get("transfer-encoding")).toBeNull();
      expect(result.get("upgrade")).toBeNull();
    });

    it("should handle empty headers", () => {
      const response = new Response("body");

      const result = buildForwardedResponseHeaders(response);

      // Response objects may have default headers like content-type
      // So we just check that hop-by-hop headers are not present
      expect(result.get("connection")).toBeNull();
      expect(result.get("upgrade")).toBeNull();
    });

    it("should forward standard response headers", () => {
      const response = new Response("body", {
        headers: {
          "content-type": "application/json",
          "content-length": "123",
          "content-encoding": "gzip",
          "content-language": "en-US",
          date: "Wed, 21 Oct 2015 07:28:00 GMT",
          expires: "Thu, 22 Oct 2015 07:28:00 GMT",
          "last-modified": "Tue, 20 Oct 2015 07:28:00 GMT"
        }
      });

      const result = buildForwardedResponseHeaders(response);

      expect(result.get("content-type")).toBe("application/json");
      expect(result.get("content-length")).toBe("123");
      expect(result.get("content-encoding")).toBe("gzip");
      expect(result.get("content-language")).toBe("en-US");
      expect(result.get("date")).toBe("Wed, 21 Oct 2015 07:28:00 GMT");
      expect(result.get("expires")).toBe("Thu, 22 Oct 2015 07:28:00 GMT");
      expect(result.get("last-modified")).toBe("Tue, 20 Oct 2015 07:28:00 GMT");
    });

    it("should forward security headers", () => {
      const response = new Response("body", {
        headers: {
          "strict-transport-security": "max-age=31536000",
          "content-security-policy": "default-src 'self'",
          "x-frame-options": "DENY",
          "x-content-type-options": "nosniff",
          "x-xss-protection": "1; mode=block"
        }
      });

      const result = buildForwardedResponseHeaders(response);

      expect(result.get("strict-transport-security")).toBe("max-age=31536000");
      expect(result.get("content-security-policy")).toBe("default-src 'self'");
      expect(result.get("x-frame-options")).toBe("DENY");
      expect(result.get("x-content-type-options")).toBe("nosniff");
      expect(result.get("x-xss-protection")).toBe("1; mode=block");
    });

    it("should forward CORS headers", () => {
      const response = new Response("body", {
        headers: {
          "access-control-allow-origin": "*",
          "access-control-allow-methods": "GET, POST, PUT",
          "access-control-allow-headers": "Content-Type, Authorization",
          "access-control-max-age": "86400",
          "access-control-expose-headers": "X-Custom-Header"
        }
      });

      const result = buildForwardedResponseHeaders(response);

      expect(result.get("access-control-allow-origin")).toBe("*");
      expect(result.get("access-control-allow-methods")).toBe("GET, POST, PUT");
      expect(result.get("access-control-allow-headers")).toBe(
        "Content-Type, Authorization"
      );
      expect(result.get("access-control-max-age")).toBe("86400");
      expect(result.get("access-control-expose-headers")).toBe(
        "X-Custom-Header"
      );
    });
  });
});

describe("url", () => {
  describe("transformTargetUrl", () => {
    /**
     * Helper to create a mock NextRequest with a given pathname and search params
     */
    function createMockRequest(
      pathname: string,
      searchParams: Record<string, string> = {}
    ): NextRequest {
      const url = new URL(`http://localhost${pathname}`);
      Object.entries(searchParams).forEach(([key, value]) => {
        url.searchParams.set(key, value);
      });

      return {
        nextUrl: url,
        headers: new Headers()
      } as unknown as NextRequest;
    }

    describe("Bug: Double path segment", () => {
      it("should not duplicate path segments when targetBaseUrl includes path", () => {
        // This is the reported bug scenario
        const req = createMockRequest("/me/v1/some-endpoint");
        const options: ProxyOptions = {
          proxyPath: "/me",
          targetBaseUrl: "https://issuer/me/v1",
          audience: "https://issuer/me/",
          scope: null
        };

        const result = transformTargetUrl(req, options);

        // The path after /me is /v1/some-endpoint
        // The targetBaseUrl already ends with /v1, so we detect and avoid duplication
        // Result: https://issuer/me/v1/some-endpoint (NOT /v1/v1/)
        expect(result.toString()).toBe("https://issuer/me/v1/some-endpoint");
      });
    });

    describe("Single segment proxy paths", () => {
      it("should handle simple single-segment proxy path", () => {
        const req = createMockRequest("/api/users");
        const options: ProxyOptions = {
          proxyPath: "/api",
          targetBaseUrl: "https://backend.example.com",
          audience: "https://backend.example.com/",
          scope: null
        };

        const result = transformTargetUrl(req, options);

        expect(result.toString()).toBe("https://backend.example.com/users");
      });

      it("should handle root path replacement", () => {
        const req = createMockRequest("/users");
        const options: ProxyOptions = {
          proxyPath: "/",
          targetBaseUrl: "https://backend.example.com",
          audience: "https://backend.example.com/",
          scope: null
        };

        const result = transformTargetUrl(req, options);

        // URL normalizes to include trailing slash
        expect(result.toString()).toBe("https://backend.example.com/users");
      });
    });

    describe("Multi-segment proxy paths", () => {
      it("should handle multi-segment proxy paths", () => {
        const req = createMockRequest("/api/v1/users");
        const options: ProxyOptions = {
          proxyPath: "/api/v1",
          targetBaseUrl: "https://backend.example.com/api/v1",
          audience: "https://backend.example.com/",
          scope: null
        };

        const result = transformTargetUrl(req, options);

        expect(result.toString()).toBe(
          "https://backend.example.com/api/v1/users"
        );
      });

      it("should handle complex nested paths", () => {
        const req = createMockRequest("/proxy/auth/v2/some/nested/endpoint");
        const options: ProxyOptions = {
          proxyPath: "/proxy/auth",
          targetBaseUrl: "https://auth.example.com/auth/v2",
          audience: "https://auth.example.com/",
          scope: null
        };

        const result = transformTargetUrl(req, options);

        // After stripping "/proxy/auth", remaining is "/v2/some/nested/endpoint"
        // targetBaseUrl ends with "/auth/v2", which overlaps with the start
        // So we avoid duplication: /auth/v2/some/nested/endpoint (not /v2/v2/)
        expect(result.toString()).toBe(
          "https://auth.example.com/auth/v2/some/nested/endpoint"
        );
      });
    });

    describe("Query parameters", () => {
      it("should preserve query parameters", () => {
        const req = createMockRequest("/api/users", {
          id: "123",
          name: "test"
        });
        const options: ProxyOptions = {
          proxyPath: "/api",
          targetBaseUrl: "https://backend.example.com",
          audience: "https://backend.example.com/",
          scope: null
        };

        const result = transformTargetUrl(req, options);

        expect(result.href).toContain("https://backend.example.com/users?");
        expect(result.searchParams.get("id")).toBe("123");
        expect(result.searchParams.get("name")).toBe("test");
      });

      it("should handle multiple query parameters", () => {
        const req = createMockRequest("/me/v1/profile", {
          format: "json",
          includeMetadata: "true"
        });
        const options: ProxyOptions = {
          proxyPath: "/me",
          targetBaseUrl: "https://issuer/me/v1",
          audience: "https://issuer/me/",
          scope: null
        };

        const result = transformTargetUrl(req, options);

        // After stripping "/me", remaining is "/v1/profile"
        // targetBaseUrl ends with "/v1", so avoid duplication
        expect(result.toString()).toMatch(
          /https:\/\/issuer\/me\/v1\/profile\?.*format=json/
        );
        expect(result.toString()).toMatch(
          /https:\/\/issuer\/me\/v1\/profile\?.*includeMetadata=true/
        );
      });
    });

    describe("Edge cases", () => {
      it("should handle proxy path with trailing slash", () => {
        const req = createMockRequest("/api/v1/test");
        const options: ProxyOptions = {
          proxyPath: "/api/v1",
          targetBaseUrl: "https://backend.example.com/api/v1/",
          audience: "https://backend.example.com/",
          scope: null
        };

        const result = transformTargetUrl(req, options);

        // The implementation removes trailing slash from base URL, so no double slash
        expect(result.toString()).toBe(
          "https://backend.example.com/api/v1/test"
        );
      });

      it("should handle target base URL without trailing slash", () => {
        const req = createMockRequest("/api/resource");
        const options: ProxyOptions = {
          proxyPath: "/api",
          targetBaseUrl: "https://backend.example.com",
          audience: "https://backend.example.com/",
          scope: null
        };

        const result = transformTargetUrl(req, options);

        expect(result.toString()).toBe("https://backend.example.com/resource");
      });

      it("should handle request path that doesn't match proxy path prefix", () => {
        const req = createMockRequest("/other/path");
        const options: ProxyOptions = {
          proxyPath: "/api",
          targetBaseUrl: "https://backend.example.com",
          audience: "https://backend.example.com/",
          scope: null
        };

        const result = transformTargetUrl(req, options);

        // If the path doesn't start with proxyPath, the entire path is used
        expect(result.toString()).toBe(
          "https://backend.example.com/other/path"
        );
      });

      it("should handle exactly matching proxy path", () => {
        const req = createMockRequest("/api");
        const options: ProxyOptions = {
          proxyPath: "/api",
          targetBaseUrl: "https://backend.example.com",
          audience: "https://backend.example.com/",
          scope: null
        };

        const result = transformTargetUrl(req, options);

        // URL naturally normalizes with trailing slash
        expect(result.toString()).toBe("https://backend.example.com/");
      });

      it("should handle path with special characters", () => {
        const req = createMockRequest("/api/user%20name/profile");
        const options: ProxyOptions = {
          proxyPath: "/api",
          targetBaseUrl: "https://backend.example.com",
          audience: "https://backend.example.com/",
          scope: null
        };

        const result = transformTargetUrl(req, options);

        expect(result.toString()).toBe(
          "https://backend.example.com/user%20name/profile"
        );
      });
    });
  });
});
